package phases

import (
	"fmt"
	"strings"

	"github.com/pkg/errors"
	bkeaddon "gopkg.openfuyao.cn/bkecommon/cluster/addon"
	confv1beta1 "gopkg.openfuyao.cn/bkecommon/cluster/api/v1beta1"
	bkeinit "gopkg.openfuyao.cn/bkecommon/cluster/initialize"
	bkenode "gopkg.openfuyao.cn/bkecommon/cluster/node"
	bkesource "gopkg.openfuyao.cn/bkecommon/source"
	ctrl "sigs.k8s.io/controller-runtime"

	bkev1beta1 "gopkg.openfuyao.cn/cluster-api-provider-bke/api/v1beta1"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/command"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/kube"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/mergecluster"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/phaseframe"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/phaseframe/phaseutil"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/utils"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/utils/annotation"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/utils/clusterutil"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/utils/condition"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/utils/constant"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/utils/scriptshelper"
)

const (
	EnsureNodesEnvName confv1beta1.BKEClusterPhase = "EnsureNodesEnv"
)

var (
	defaultEnvExtraExecScripts = []string{
		"install-lxcfs.sh",
		"install-nfsutils.sh",
		"install-etcdctl.sh",
		"install-helm.sh",
		"install-calicoctl.sh",
		"update-runc.sh",
		"clean-docker-images.py",
	}

	commonEnvExtraExecScripts = []string{
		"file-downloader.sh",
		"package-downloader.sh",
	}
)

type EnsureNodesEnv struct {
	phaseframe.BasePhase
	nodes bkenode.Nodes
}

func NewEnsureNodesEnv(ctx *phaseframe.PhaseContext) phaseframe.Phase {
	base := phaseframe.NewBasePhase(ctx, EnsureNodesEnvName)
	return &EnsureNodesEnv{BasePhase: base}
}

func (e *EnsureNodesEnv) Execute() (_ ctrl.Result, err error) {
	return e.CheckOrInitNodesEnv()
}

func (e *EnsureNodesEnv) NeedExecute(old *bkev1beta1.BKECluster, new *bkev1beta1.BKECluster) (needExecute bool) {
	if !e.BasePhase.DefaultNeedExecute(old, new) {
		return false
	}

	needExecute = phaseutil.GetNeedInitEnvNodes(new).Length() > 0
	if needExecute {
		e.SetStatus(bkev1beta1.PhaseWaiting)
	}
	return needExecute
}

func (e *EnsureNodesEnv) getNodesToInitEnv() bkenode.Nodes {
	_, _, bkeCluster, _, _ := e.Ctx.Untie()
	nodes := phaseutil.GetNeedInitEnvNodes(bkeCluster)
	var exceptEnvNodes bkenode.Nodes

	for _, node := range nodes {
		if !bkeCluster.GetNodeStateFlag(node.IP, bkev1beta1.NodeAgentReadyFlag) {
			continue
		}
		exceptEnvNodes = append(exceptEnvNodes, node)
		bkeCluster.SetNodeStateWithMessage(node.IP, bkev1beta1.NodeInitializing, "Initializing node env")
	}

	return exceptEnvNodes
}

func (e *EnsureNodesEnv) setupClusterConditionAndSync() error {
	_, c, bkeCluster, _, log := e.Ctx.Untie()
	condition.ConditionMark(bkeCluster, bkev1beta1.NodesEnvCondition, confv1beta1.ConditionFalse, constant.NodesEnvNotReadyReason, "")
	if err := mergecluster.SyncStatusUntilComplete(c, bkeCluster); err != nil {
		log.Error(constant.NodesEnvNotReadyReason, "Failed to sync status: %v", err)
		return err
	}
	return nil
}

func (e *EnsureNodesEnv) buildEnvCommand(exceptEnvNodes bkenode.Nodes) (*command.ENV, error) {
	ctx, c, bkeCluster, scheme, log := e.Ctx.Untie()
	extra, extraHosts := e.getExtraAndExtraHosts(bkeCluster)

	timeOut, err := phaseutil.GetBootTimeOut(bkeCluster)
	if err != nil {
		log.Warn(constant.NodesEnvNotReadyReason, "Get boot timeout failed. err: %v", err)
	}

	deepRestore := e.shouldUseDeepRestore(bkeCluster)

	envCmd := &command.ENV{
		BaseCommand: command.BaseCommand{
			Ctx:             ctx,
			NameSpace:       bkeCluster.Namespace,
			Client:          c,
			Scheme:          scheme,
			OwnerObj:        bkeCluster,
			ClusterName:     bkeCluster.Name,
			Unique:          true,
			RemoveAfterWait: true,
			WaitTimeout:     timeOut,
		},
		Nodes:         exceptEnvNodes,
		BkeConfigName: bkeCluster.Name,
		Extra:         extra,
		ExtraHosts:    extraHosts,
		DryRun:        bkeCluster.Spec.DryRun,
		DeepRestore:   deepRestore,
	}

	if err := envCmd.New(); err != nil {
		errInfo := fmt.Sprintf("failed to create k8s env init command: %v", err)
		log.Error(constant.CommandCreateFailedReason, errInfo)
		return nil, err
	}

	return envCmd, nil
}

// shouldUseDeepRestore 判断是否启用 deep restore
func (e *EnsureNodesEnv) shouldUseDeepRestore(bkeCluster *bkev1beta1.BKECluster) bool {
	v, ok := annotation.HasAnnotation(bkeCluster, annotation.DeepRestoreNodeAnnotationKey)
	return (ok && v == "true") || !ok
}

func (e *EnsureNodesEnv) getExtraAndExtraHosts(bkeCluster *bkev1beta1.BKECluster) ([]string, []string) {
	var extra []string
	var extraHosts []string

	ep := bkeCluster.Spec.ControlPlaneEndpoint
	nodes := bkeCluster.Spec.ClusterConfig.Nodes
	if clusterutil.AvailableLoadBalancerEndPoint(ep, nodes) {
		extra = append(extra, ep.Host)
		extraHosts = append(extraHosts, fmt.Sprintf("%s:%s", constant.MasterHADomain, ep.Host))
	}

	if ingressVip, _ := clusterutil.GetIngressConfig(bkeCluster.Spec.ClusterConfig.Addons); ingressVip != "" && ingressVip != ep.Host {
		extra = append(extra, ingressVip)
	}

	return extra, extraHosts
}

func (e *EnsureNodesEnv) executeEnvCommand(envCmd *command.ENV) (error, []string, []string) {
	_, _, _, _, log := e.Ctx.Untie()
	log.Info(constant.NodesEnvCheckingReason, "Waiting for the env check to complete")
	return envCmd.Wait()
}

func (e *EnsureNodesEnv) handleSuccessNodes(successNodes []string) {
	_, _, bkeCluster, _, _ := e.Ctx.Untie()
	for _, node := range successNodes {
		nodeIP := phaseutil.GetNodeIPFromCommandWaitResult(node)
		bkeCluster.MarkNodeStateFlag(nodeIP, bkev1beta1.NodeEnvFlag)
		bkeCluster.SetNodeStateMessage(nodeIP, "Nodes env is ready")

		filtered := e.nodes.Filter(bkenode.FilterOptions{"IP": nodeIP})
		if len(filtered) > 0 {
			e.nodes = append(e.nodes, filtered[0])
		}
	}
}

func (e *EnsureNodesEnv) handleFailedNodes(envCmd *command.ENV, failedNodes []string) error {
	_, _, bkeCluster, _, log := e.Ctx.Untie()
	for _, node := range failedNodes {
		nodeIP := phaseutil.GetNodeIPFromCommandWaitResult(node)
		bkeCluster.SetNodeStateWithMessage(nodeIP, bkev1beta1.NodeInitFailed, "Failed to check k8s env")
		bkeCluster.SetSkipNodeError(nodeIP)
	}

	commandErrs, err := phaseutil.LogCommandFailed(*envCmd.Command, failedNodes, log, constant.NodesEnvNotReadyReason)
	phaseutil.MarkNodeStatusByCommandErrs(bkeCluster, commandErrs)

	if len(failedNodes) > 0 {
		log.Error(constant.NodesEnvNotReadyReason, "failed to check k8s env in following nodes: %v", failedNodes)
	}

	return err
}

func (e *EnsureNodesEnv) finalDecisionAndCleanup(successNodes, failedNodes []string) (ctrl.Result, error) {
	_, c, bkeCluster, _, log := e.Ctx.Untie()
	if err := mergecluster.SyncStatusUntilComplete(c, bkeCluster); err != nil {
		return ctrl.Result{}, err
	}

	if len(successNodes) == 0 {
		errMsg := fmt.Sprintf("failed to check k8s env in all nodes: %v", failedNodes)
		log.Error(constant.NodesEnvNotReadyReason, errMsg)
		return ctrl.Result{}, errors.New(errMsg)
	}

	e.initClusterExtra()

	if bkeCluster.Status.ClusterHealthState == bkev1beta1.Deploying && len(failedNodes) > 0 {
		needCountFailed := phaseutil.GetNotSkipFailedNode(bkeCluster, failedNodes)
		if needCountFailed > 0 {
			errMsg := fmt.Sprintf("At Deploying state, not skip nodes need init env success, retry later. failed count: %d", needCountFailed)
			log.Info(constant.NodesEnvUpdatingReason, errMsg)
			return ctrl.Result{}, errors.New(errMsg)
		}
	}

	log.Info(constant.NodesEnvCheckingReason, "The env check is complete")
	condition.ConditionMark(bkeCluster, bkev1beta1.NodesEnvCondition, confv1beta1.ConditionTrue, constant.NodesEnvReadyReason, "")
	return ctrl.Result{}, nil
}

func (e *EnsureNodesEnv) CheckOrInitNodesEnv() (ctrl.Result, error) {
	_, _, _, _, log := e.Ctx.Untie()
	log.Info("CheckAndInitNodeEnv", "Start check and init node env for k8s")

	exceptEnvNodes := e.getNodesToInitEnv()
	if exceptEnvNodes.Length() == 0 {
		log.Info("CheckAndInitNodeEnv", "No more node need to init env")
		return ctrl.Result{}, nil
	}

	if err := e.setupClusterConditionAndSync(); err != nil {
		return ctrl.Result{}, err
	}

	envCmd, err := e.buildEnvCommand(exceptEnvNodes)
	if err != nil {
		return ctrl.Result{}, err
	}

	err, successNodes, failedNodes := e.executeEnvCommand(envCmd)
	if err != nil {
		errInfo := fmt.Sprintf("failed to check k8s env: %v", err)
		log.Error(constant.NodesEnvNotReadyReason, errInfo)
		return ctrl.Result{}, err
	}

	e.handleSuccessNodes(successNodes)

	if handleErr := e.handleFailedNodes(envCmd, failedNodes); handleErr != nil {
		errInfo := fmt.Sprintf("handle failed nodes failed: %v", handleErr)
		log.Error(constant.NodesEnvNotReadyReason, errInfo)
	}

	return e.finalDecisionAndCleanup(successNodes, failedNodes)
}

func (e *EnsureNodesEnv) initClusterExtra() {
	ctx, _, bkeCluster, _, log := e.Ctx.Untie()

	localClient, err := kube.NewClientFromRestConfig(ctx, e.Ctx.RestConfig)
	if err != nil {
		log.Warn(constant.EnvExtraExecScriptFailed, "获取k8s client 失败, 跳过自定义脚本执行, err: %v", err)
		return
	}

	scriptsLi, err := scriptshelper.ListScriptsConfigMaps(e.Ctx.Client)
	if err != nil {
		log.Warn(constant.InternalErrorReason, "获取环境初始化自定义脚本 configmaps 失败, 跳过自定义脚本执行, err: %v", err)
		return
	}
	cfg := bkeinit.BkeConfig(*bkeCluster.Spec.ClusterConfig)

	// 执行common scripts 的安装 这一步是默认的
	for _, script := range commonEnvExtraExecScripts {
		if !utils.ContainsString(scriptsLi, script) {
			log.Warn(constant.EnvExtraExecScriptFailed, "基础脚本 %q 不存在, 跳过自定义脚本执行", script)
			return
		}

		nodesIps, err := e.getNodesIpsByScript(script)
		if err != nil {
			log.Warn(constant.EnvExtraExecScriptSkip, "获取安装基础脚本 %q 节点IP失败, 跳过自定义脚本执行, err: %v", script, err)
			return
		}
		if len(nodesIps) == 0 {
			log.Warn(constant.EnvExtraExecScriptSkip, "获取安装基础脚本 %q 节点IP为空, 跳过自定义脚本执行", script)
			return
		}

		param := map[string]string{
			"nodesIps": nodesIps,
		}

		addonT := &bkeaddon.AddonTransfer{
			Addon: &confv1beta1.Product{
				Name:    "clusterextra",
				Version: script,
				Param:   param,
				Block:   false,
			},
			Operate: bkeaddon.CreateAddon,
		}

		if err := localClient.InstallAddon(bkeCluster, addonT, nil); err != nil {
			log.Warn(constant.EnvExtraExecScriptFailed, "安装基础脚本 %q 失败, 跳过自定义脚本执行, err: %v", script, err)
			return
		}

		log.Info(constant.EnvExtraExecScriptSuccess, "额外安装基础脚本 %q 成功", script)
	}

	// 执行其他脚本
	otherCustomScripts := defaultEnvExtraExecScripts

	scriptsScope, ok := cfg.CustomExtra["envExtraExecScripts"]
	if ok {
		otherCustomScripts = strings.Split(scriptsScope, ",")
	}

	httpRepo := bkesource.GetCustomDownloadPath(cfg.YumRepo())

	for _, script := range otherCustomScripts {
		if !utils.ContainsString(scriptsLi, script) {
			log.Warn(constant.EnvExtraExecScriptSkip, "自定义脚本 %q 不存在, 跳过自定义脚本执行", script)
			continue
		}

		if script == "update-runc.sh" && cfg.Cluster.ContainerRuntime.CRI == bkeinit.CRIContainerd {
			log.Info(constant.EnvExtraExecScriptSkip, "自定义脚本 %q 不支持 containerd, 跳过自定义脚本执行", script)
			continue
		}

		nodesIps, err := e.getNodesIpsByScript(script)
		if err != nil {
			log.Warn(constant.EnvExtraExecScriptSkip, "获取自定义脚本 %q 节点IP失败, 跳过自定义脚本执行, err: %v", script, err)
			continue
		}
		if len(nodesIps) == 0 {
			log.Warn(constant.EnvExtraExecScriptSkip, "获取自定义脚本 %q 节点IP为空, 跳过自定义脚本执行", script)
			continue
		}

		param := map[string]string{
			"nodesIps": nodesIps,
			"httpRepo": httpRepo,
		}

		addonT := &bkeaddon.AddonTransfer{
			Addon: &confv1beta1.Product{
				Name:    "clusterextra",
				Version: script,
				Param:   param,
				Block:   false,
			},
			Operate: bkeaddon.CreateAddon,
		}

		if script == "update-runc.sh" {
			addonT.Addon.Block = true
		}

		if err := localClient.InstallAddon(bkeCluster, addonT, nil); err != nil {
			log.Warn(constant.EnvExtraExecScriptFailed, "执行自定义脚本 %q 失败, 跳过自定义脚本执行, err: %v", script, err)
			continue
		}

		log.Info(constant.EnvExtraExecScriptSuccess, "额外执行自定义脚本 %q 成功", script)
	}
}

func (e *EnsureNodesEnv) getNodesIpsByScript(script string) (string, error) {
	//"install-lxcfs.sh",
	//"install-nfsutils.sh",
	//"install-etcdctl.sh",
	//"install-helm.sh",
	//"install-calicoctl.sh",
	//"update-runc.sh",
	//"clean-docker-images.py",
	//"file-downloader.sh",
	//"package-downloader.sh",

	masterNodes := e.nodes.Master()
	etcdNodes := e.nodes.Etcd()

	allNodesIps := make([]string, e.nodes.Length())
	etcdNodesIps := make([]string, etcdNodes.Length())
	masterNodesIps := make([]string, masterNodes.Length())
	for i, node := range e.nodes {
		allNodesIps[i] = node.IP
	}
	for i, node := range etcdNodes {
		etcdNodesIps[i] = node.IP
	}
	for i, node := range masterNodes {
		masterNodesIps[i] = node.IP
	}

	switch script {
	case "file-downloader.sh":
		return strings.Join(allNodesIps, ","), nil
	case "package-downloader.sh":
		return strings.Join(allNodesIps, ","), nil

	case "install-lxcfs.sh":
		return strings.Join(allNodesIps, ","), nil
	case "install-nfsutils.sh":
		if v, ok := e.Ctx.BKECluster.Spec.ClusterConfig.CustomExtra["pipelineServer"]; ok {
			return v, nil
		}
		return "", errors.Errorf("没有在 Spec.ClusterConfig.CustomExtra 中配置 pipelineServer")
	case "install-etcdctl.sh":
		return strings.Join(etcdNodesIps, ","), nil
	case "install-helm.sh":
		return strings.Join(masterNodesIps, ","), nil
	case "install-calicoctl.sh":
		return strings.Join(masterNodesIps, ","), nil
	case "update-runc.sh":
		if v, ok := e.Ctx.BKECluster.Spec.ClusterConfig.CustomExtra["host"]; ok && v != "" {
			updateRuncNodesIps := make([]string, 0)
			for _, node := range allNodesIps {
				if node == v {
					continue
				}
				updateRuncNodesIps = append(updateRuncNodesIps, node)
			}
			return strings.Join(updateRuncNodesIps, ","), nil
		}

		return strings.Join(allNodesIps, ","), nil

	case "clean-docker-images.py":
		nodes := ""
		if v, ok := e.Ctx.BKECluster.Spec.ClusterConfig.CustomExtra["pipelineServer"]; ok {
			nodes = v
		} else {
			return "", errors.Errorf("没有在 Spec.ClusterConfig.CustomExtra 中配置 pipelineServer")
		}

		if v, ok := e.Ctx.BKECluster.Spec.ClusterConfig.CustomExtra["pipelineServerEnableCleanImages"]; ok && v == "true" {
			return nodes, nil
		}

		return "", errors.Errorf("没有在 Spec.ClusterConfig.CustomExtra 中配置 pipelineServerEnableCleanImages")
	default:
		return strings.Join(allNodesIps, ","), nil
	}
}
